﻿using System;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Represents a tone (pitch).
    /// </summary>
    /// <remarks>
    /// <para>
    /// A tone is expressed as a combination of three values:
    /// <list type="bullet">
    /// <item>
    ///     <see cref="Note"/> that expresses the pitch class of the tone (relative pitch).<br/>
    ///     This value ranges from 1 to 7 and corresponds to one of the 7 basic notes (C, D, E, F, G, A, B [H]).
    /// </item>
    /// <item>
    ///     <see cref="Octave"/> that defines the absolute pitch of the tone (octave + note = absolute pitch)
    /// </item>
    /// <item>
    ///     <see cref="Accidental"/> that lowers or raises the tone by up to one tone.
    /// </item>
    /// </list>
    /// </para>
    /// <para>
    /// A tone is just a representation of a pitch. It does not have any notion of a duration.
    /// Therefore it is intended to be an owned member of a <see cref="DurationalSymbolElement"/>.
    /// </para>
    /// <para>
    /// The class implements the <see cref="IComparable{T}"/> interface. Thus two <see cref="ToneElement"/>
    /// instances can be compared for their pitch.
    /// </para>
    /// </remarks>
    /// <seealso cref="DurationalSymbolElement"/>
    /// <seealso cref="ChordElement"/>
    public class ToneElement : ModelElement, IComparable<ToneElement>, IComparable<Tone>
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ToneElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ChordElement"/>
        /// </para>
        /// <para>
        /// The chord that directly owns this tone.
        /// </para>
        /// </param>
        /// <param name="note">
        /// <para>
        /// Type: <see cref="System.Byte"/>
        /// </para>
        /// <para>
        /// The note of the tone.
        /// </para>
        /// </param>
        /// <param name="octave">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The octave of the tone.
        /// </para>
        /// </param>
        /// <param name="accidental">
        /// <para>
        /// Type: <see cref="System.Double"/>
        /// </para>
        /// <para>
        /// The accidental of the tone.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The value of <paramref name="note"/>, <paramref name="octave"/> or <paramref name="accidental"/> is not valid.
        /// Please, refer to the documentation of the respective properties for details.
        /// </exception>
        internal ToneElement(CompositionPart packagePart, ChordElement owner, byte note, int octave, double accidental)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            Note = note;
            Octave = octave;
            Accidental = accidental;
        }

        /// <summary>
        /// Creates a new <see cref="ToneElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ChordElement"/>
        /// </para>
        /// <para>
        /// The chord that directly owns this tone.
        /// </para>
        /// </param>
        /// <param name="tone">
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// A <see cref="Tone"/> structure that contains the values that should be assigned to this element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The values of <paramref name="tone"/> fields are not valid.
        /// Please, refer to the documentation of the respective properties for details.
        /// </exception>
        internal ToneElement(CompositionPart packagePart, ChordElement owner, Tone tone)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            Note = tone.Note;
            Octave = tone.Octave;
            Accidental = tone.Accidental;
        }

        /// <summary>
        /// Creates a new <see cref="ToneElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ChordElement"/>
        /// </para>
        /// <para>
        /// The chord that directly owns this tone.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        internal ToneElement(CompositionPart packagePart, ChordElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            accidentalAttribute = element.Attributes[PackageVocabulary.AccidentalAttribute];
            noteAttribute = element.Attributes[PackageVocabulary.NoteAttribute];
            octaveAttribute = element.Attributes[PackageVocabulary.OctaveAttribute];
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.ToneElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            accidentalAttribute = document.CreateAttribute(PackageVocabulary.AccidentalAttribute);
            noteAttribute = document.CreateAttribute(PackageVocabulary.NoteAttribute);
            octaveAttribute = document.CreateAttribute(PackageVocabulary.OctaveAttribute);

            element.Attributes.Append(accidentalAttribute);
            element.Attributes.Append(noteAttribute);
            element.Attributes.Append(octaveAttribute);

            return element;
        }

        #endregion

        #region IComparable<ToneElement> Members

        /// <inheritdoc/>
        public int CompareTo(ToneElement other)
        {
            if (other.Octave > Octave)
                return -1;
            else if (other.Octave < Octave)
                return 1;
            else
            {
                if (other.Note + other.Accidental > Note + Accidental)
                    return -1;
                else if (other.Note + other.Accidental < Note + Accidental)
                    return 1;
                else
                    return 0;
            }
        }

        #endregion

        #region IComparable<Tone> Members

        /// <inheritdoc/>
        public int CompareTo(Tone other)
        {
            if (other.Octave > Octave)
                return -1;
            else if (other.Octave < Octave)
                return 1;
            else
            {
                if (other.Note + other.Accidental > Note + Accidental)
                    return -1;
                else if (other.Note + other.Accidental < Note + Accidental)
                    return 1;
                else
                    return 0;
            }
        }

        #endregion

        #region Object Members

        /// <inheritdoc/>
        public override bool Equals(object obj)
        {
            return (obj is ToneElement) && (this == (ToneElement)obj);
        }

        /// <inheritdoc/>
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        #endregion

        #region Operators

        /// <summary>
        /// Compares two <see cref="ToneElement"/> instances for pitch inequality.
        /// </summary>
        /// <param name="t1">First compared instance</param>
        /// <param name="t2">Second compared instance</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="t1"/> has lower pitch than <paramref name="t2"/>, <see langword="false"/> otherwise.
        /// </returns>
        public static bool operator <(ToneElement t1, ToneElement t2)
        {
            return t1.CompareTo(t2) < 0;
        }

        /// <summary>
        /// Compares two <see cref="ToneElement"/> instances for pitch inequality.
        /// </summary>
        /// <param name="t1">First compared instance</param>
        /// <param name="t2">Second compared instance</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="t1"/> has higher pitch than <paramref name="t2"/>, <see langword="false"/> otherwise.
        /// </returns>
        public static bool operator >(ToneElement t1, ToneElement t2)
        {
            return t1.CompareTo(t2) > 0;
        }

        /// <summary>
        /// Compares two <see cref="ToneElement"/> instances for pitch equality.
        /// </summary>
        /// <param name="t1">First compared instance</param>
        /// <param name="t2">Second compared instance</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="t1"/> has the same pitch as <paramref name="t2"/>, <see langword="false"/> otherwise.
        /// </returns>
        public static bool operator ==(ToneElement t1, ToneElement t2)
        {
            if ((object)t1 == null)
                return (object)t2 == null;
            else if ((object)t2 == null)
                return false;

            return t1.CompareTo(t2) == 0;
        }

        /// <summary>
        /// Compares two <see cref="ToneElement"/> instances for pitch inequality.
        /// </summary>
        /// <param name="t1">First compared instance</param>
        /// <param name="t2">Second compared instance</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="t1"/> has different pitch than <paramref name="t2"/>, <see langword="false"/> otherwise.
        /// </returns>
        public static bool operator !=(ToneElement t1, ToneElement t2)
        {
            return !(t1 == t2);
        }

        /// <summary>
        /// Compares two <see cref="ToneElement"/> instances for pitch inequality.
        /// </summary>
        /// <param name="t1">First compared instance</param>
        /// <param name="t2">Second compared instance</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="t1"/> has lower or equal pitch than <paramref name="t2"/>, <see langword="false"/> otherwise.
        /// </returns>
        public static bool operator <=(ToneElement t1, ToneElement t2)
        {
            return t1.CompareTo(t2) <= 0;
        }

        /// <summary>
        /// Compares two <see cref="ToneElement"/> instances for pitch inequality.
        /// </summary>
        /// <param name="t1">First compared instance</param>
        /// <param name="t2">Second compared instance</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="t1"/> has higher or equal pitch than <paramref name="t2"/>, <see langword="false"/> otherwise.
        /// </returns>
        public static bool operator >=(ToneElement t1, ToneElement t2)
        {
            return t1.CompareTo(t2) >= 0;
        }

        #endregion

        #region Public Constants

        /// <summary>
        /// Identifies the C note.
        /// </summary>
        public const int C = 1;

        /// <summary>
        /// Identifies the D note.
        /// </summary>
        public const int D = 2;

        /// <summary>
        /// Identifies the E note.
        /// </summary>
        public const int E = 3;

        /// <summary>
        /// Identifies the F note.
        /// </summary>
        public const int F = 4;

        /// <summary>
        /// Identifies the G note.
        /// </summary>
        public const int G = 5;

        /// <summary>
        /// Identifies the A note.
        /// </summary>
        public const int A = 6;

        /// <summary>
        /// Identifies the B note.
        /// </summary>
        public const int B = 7;

        /// <summary>
        /// Identifies the Double Flat accidental.
        /// </summary>
        public const double DoubleFlat = -1.0;

        /// <summary>
        /// Identifies the Flat accidental.
        /// </summary>
        public const double Flat = -0.5;

        /// <summary>
        /// Identifies the Neutral accidental.
        /// </summary>
        public const double Neutral = 0.0;

        /// <summary>
        /// Identifies the Sharp accidental.
        /// </summary>
        public const double Sharp = 0.5;

        /// <summary>
        /// Identifies the Double Sharp accidental.
        /// </summary>
        public const double DoubleSharp = 1.0;

        #endregion

        #region Public Methods

        /// <summary>
        /// Returns a <see cref="Tone"/> structure initialized to the values of this <see cref="ToneElement"/>.
        /// </summary>
        /// <returns>
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// The tone structures with the values of this <see cref="ToneElement"/>.
        /// </para>
        /// </returns>
        public Tone GetToneStruct()
        {
            return new Tone()
            {
                Accidental = this.Accidental,
                Note = this.Note,
                Octave = this.Octave
            };
        }

        /// <summary>
        /// Modifies this tone so that it corresponds to a given one.
        /// </summary>
        /// <param name="tone">
        /// <para>
        /// Type: <see cref="Tone"/>
        /// </para>
        /// <para>
        /// The reference tone.
        /// </para>
        /// </param>
        /// <exception cref="ValidationException">
        /// The <paramref name="tone"/>.<see cref="Tone.Note"/> or <paramref name="tone"/>.<see cref="Tone.Accidental"/>
        /// field has not a valid value.
        /// </exception>
        public void SetTone(Tone tone)
        {
            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            ValidateAndThrow("Note", tone.Note);
            ValidateAndThrow("Accidental", tone.Accidental);

            noteAttribute.Value = tone.Note.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            octaveAttribute.Value = tone.Octave.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            accidentalAttribute.Value = tone.Accidental.ToString(PackageVocabulary.PackageCulture.NumberFormat);

            RaisePropertyChanged("Note");
            RaisePropertyChanged("Octave");
            RaisePropertyChanged("Accidental");
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gives the value of the accidental that modifies this tone.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Double"/>
        /// </para>
        /// <para>
        /// The accidental lowers or raises a note by up to one tone.
        /// </para>
        /// <para>
        /// In this representation the accidental is a real number ranging from -1.0 to 1.0
        /// where 0.0 stands for no change, 0.5 would change the note by one semitone and 0.25 by one quarter tone.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The value is not from the range [-1.0, 1.0].
        /// </exception>
        /// TODO: Write the validation rules
        public double Accidental
        {
            get { return double.Parse(accidentalAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("Accidental", value);
                (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

                accidentalAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("Accidental");
            }
        }

        /// <summary>
        /// Gets or sets the octave of this tone.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// An octave is the interval between one musical pitch and another with half or double its frequency.
        /// </para>
        /// <para>
        /// 1 stands for a one-lined octave (the one which starts with middle c ~ 261.63Hz).<br/>
        /// Smaller values represents lower octaves, greater values represents higher octaves,
        /// e.g. 3 stands for three-lined octave (c ~ 1046.50Hz). 
        /// </para>
        /// </value>
        /// TODO: Write the validation rules
        public int Octave
        {
            get { return int.Parse(octaveAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

                octaveAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("Octave");
            }
        }

        /// <summary>
        /// Gets or sets the name of the note (in the chromatic system).
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Byte"/>
        /// </para>
        /// <para>
        /// Two notes with fundamental frequencies in a ratio of any power of two 
        /// (e.g. half, twice, or four times) are perceived as very similar. 
        /// Because of that, all notes with these kinds of relations can be grouped 
        /// under the same pitch class.
        /// </para>
        /// <para>
        /// The pitch class in this representation is expressed as an integer ranging from 1 to 7,
        /// thus giving a scale of 7 tones (C, D, E, F, G, A, B [H]).
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The value is not in the range [1, 7].
        /// </exception>
        /// <seealso cref="Octave"/>
        /// <seealso cref="Accidental"/>
        /// TODO: Write the validation rules
        public byte Note
        {
            get { return byte.Parse(noteAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("Note", value);

                (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

                noteAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("Note");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// The XML attribute that holds the value of the <see cref="Accidental"/> property.
        /// </summary>
        private XmlAttribute accidentalAttribute;
        /// <summary>
        /// The XML attribute that holds the value of the <see cref="Octave"/> property.
        /// </summary>
        private XmlAttribute octaveAttribute;
        /// <summary>
        /// The XML attribute that holds the value of the <see cref="Note"/> property.
        /// </summary>
        private XmlAttribute noteAttribute;

        #endregion
    }
}
